---
title: Faults and error pipeline
summary: SoapException behavior, exception handling, and what a caller receives when an ADUserServices operation fails.
status: draft
owner: Not determined from source
last_reviewed: 2026-09-22
---

# Faults and error pipeline

`clsADUserServices` has no fault contract mechanism of its own — ASMX offers none — and no `SoapException` is ever constructed in source. Most operations catch a directory or password failure and return an `ADServiceResponse` sentinel instead of throwing; a smaller set of operations have no `Try`/`Catch` at all, so an escaping .NET exception reaches ASP.NET's default wrapping and surfaces as a `SoapException`. Several sentinel-returning operations catch and **swallow** the failure entirely, producing a response indistinguishable from success.

## Fault surface summary

| Aspect | Finding |
| --- | --- |
| Fault contracts | Not applicable — ASMX has no fault contract mechanism |
| Exception wrapping | ASP.NET wraps an escaping exception in `SoapException` |
| Custom `SoapException` throwing | None — no `SoapException` is constructed anywhere in `clsADUserServices.asmx.vb` |
| Global handler | None — no `Global.asax` exists in the repository, so there is no `Application_Error` |
| Detail exposure | No `<customErrors>` element appears in `src\ADUserServices\Web.config` — the effective detail-exposure mode is a framework default and is **not determined from source**.** `<compilation debug="true">` is present in the checked-in `Web.config` and is removed only by the `Web.Release.config` transform (see [tech-debt.md](tech-debt.md) TD-13) |
| Sentinel-value failures | 14 of 19 operations return a sentinel value on failure instead of throwing (see below) |
| Effect on callers | A caller cannot rely on a uniform failure signal: some operations fault, most return `ADServiceResponse{Response=Failure}` with `ex.Message` copied into `Message`, and a subset **swallow** the failure and return `Success` or an empty collection indistinguishable from a legitimate empty result |

## Exception handling by operation

| Operation | Handling | What the caller receives | Logged |
| --- | --- | --- | --- |
| [`HelloWorld`](contracts/clsaduserservices.md#helloworld) | No failure path exists — constant return | `"Hello World"` | No |
| [`AddUser`](contracts/clsaduserservices.md#adduser) | Caught and converted to a return value inside two `Try` blocks; unhandled outside them | `ADServiceResponse{Failure}` \| `SoapException` (context/`New UserPrincipal`/property-assignment failures) | No |
| [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) | Caught and converted — whole body in one `Try`/`Catch` | `ADServiceResponse{Failure, ex.Message}` | No |
| [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup) | Caught and converted | `ADServiceResponse{UserAlreadyInGroup \| Failure, ex.Message}` | No |
| [`DeleteUser`](contracts/clsaduserservices.md#deleteuser) | Caught and converted; missing-user path falls through with no exception at all | `ADServiceResponse{Success \| Failure, ex.Message}` | No |
| [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) | **Caught and swallowed** — authored comment `'gobble - user is pribably not in the group` | `ADServiceResponse{Success}` — indistinguishable from a genuine removal | No |
| [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist) | **Caught and swallowed** inside the shared `GetUserPrincipal` helper (H2), into `m_LastMessage`, never read | `False` — indistinguishable from "user absent" | No |
| [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource) | Unhandled — no `Try`/`Catch` around `GetAuthorizationGroups()` | `SoapException` | No |
| [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| Failure, ex.Message}` | No |
| [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword) | Caught and converted — wraps the private `ChangePassword` helper (H6) | `ADServiceResponse{Failure, ex.Message}` | No |
| [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) | Unhandled — no `Try`/`Catch` around H6; deliberately faulting by design | `SoapException` | No |
| [`GetUserEmail`](contracts/clsaduserservices.md#getuseremail) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| Failure, ex.Message}` | No |
| [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups) | **Caught and swallowed** — empty `Catch` block | Empty `ArrayList` — indistinguishable from "no groups" | No |
| [`GetUserResources`](contracts/clsaduserservices.md#getuserresources) | **Caught and swallowed** — empty `Catch` block | Empty `ArrayList` — indistinguishable from "no matching groups" | No |
| [`GetUserFAX`](contracts/clsaduserservices.md#getuserfax) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| Failure, ex.Message}` | No |
| [`GetUserLastLogon`](contracts/clsaduserservices.md#getuserlastlogon) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| InfoNotAvailable \| Failure, ex.Message}` | No |
| [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| Failure, ex.Message}` | No |
| [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup) | Caught and converted | `ADServiceResponse{UserDoesNotExist \| GroupDoesNotExist \| Failure, ex.Message}` | No |
| [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) | Unhandled — no `Try`/`Catch` around `GetAllowedIPs()` | `SoapException` | No |

No operation logs on failure — see *Logging on failure* below. The "caught and swallowed" rows are the most consequential findings on this page: the caller receives an apparently successful (or empty) response that is indistinguishable from a genuine result. Each is raised in [tech-debt.md](tech-debt.md) (`DeleteUserFromGroup` TD-20, `GetUserGroups`/`GetUserResources` TD-21, `DoesUserExist` TD-22).

## Error pipeline diagram

```mermaid
flowchart TD
    OP["Operation body raises<br>a .NET exception"] --> CATCH{"Caught in<br>the operation?"}
    CATCH -->|"No — 4 operations"| WRAP["ASP.NET wraps it<br>in SoapException"]
    CATCH -->|"Yes — 14 operations"| HANDLED{"Handling"}
    HANDLED -->|"Convert to sentinel"| SENTINEL["ADServiceResponse<br>Failure plus ex.Message"]
    HANDLED -->|"Swallow<br>DeleteUserFromGroup /<br>GetUserGroups / GetUserResources /<br>DoesUserExist"| SILENT["Response indistinguishable<br>from success or empty result"]
    WRAP --> DETAIL{"customErrors mode"}
    DETAIL -->|"Not declared in Web.config"| UNKNOWN["Detail exposure is a<br>framework default -<br>not determined from source"]
    UNKNOWN --> CALLER["Caller receives SOAP fault"]
```

## Custom SOAP faults

No operation constructs a `SoapException` anywhere in `clsADUserServices.asmx.vb`. Every fault a caller observes on this service is framework-generated from an unhandled .NET exception escaping one of the four operations listed in *Exception handling by operation* that carry no `Try`/`Catch`: `AddUser` (outside its two guarded blocks), `DoesUserHaveResource`, `ChangeUserPasswordOrGetException`, and `CheckAccess`.

## Sentinel-value failure conventions

| Operation | Sentinel | Meaning | Ambiguity |
| --- | --- | --- | --- |
| [`AddUser`](contracts/clsaduserservices.md#adduser) | `ADServiceResponse{Response=Failure}` | Access denied, existing user, or a caught save/commit failure | The existing-user code path (`UserExists`) is set then immediately overwritten to `Failure`, so a duplicate-user attempt is indistinguishable from any other failure ([tech-debt.md](tech-debt.md) TD-17) |
| [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) | `Response=Success` with free-text `Message` | Unlocked, already-unlocked, or unlock-did-not-take-effect | The three outcomes are distinguished only in free text, not in `Response` |
| [`DeleteUser`](contracts/clsaduserservices.md#deleteuser) | `Response=Success` | Deleted, or the user was never found | A missing user and a successful delete are indistinguishable; a directory-lookup failure inside `GetUserPrincipal` is also swallowed into this same `Success` |
| [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) | `Response=Success` | Removed, was not a member, **or** a genuine directory failure | All three collapse to `Success` via an explicit swallow (`'gobble`) ([tech-debt.md](tech-debt.md) TD-20, **HIGH**) |
| [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist) | `False` | User absent, or directory lookup failed | Both collapse to `False`; the exception is captured in `m_LastMessage`, which no caller can read ([tech-debt.md](tech-debt.md) TD-22) |
| [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups) | Empty `ArrayList` | No groups, no such user, or directory failure | All three collapse to an empty list via an empty `Catch` block ([tech-debt.md](tech-debt.md) TD-21, **HIGH**) |
| [`GetUserResources`](contracts/clsaduserservices.md#getuserresources) | Empty `ArrayList` | No matching resources, no such user, or directory failure | Same collapse as `GetUserGroups` ([tech-debt.md](tech-debt.md) TD-21, **HIGH**) |
| [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) | `Response=Success` | A request that changes only `inCompany`/`inInfo` reports success but commits nothing, because those two writes never set the `modified` flag | Indistinguishable from a genuine successful update ([tech-debt.md](tech-debt.md) TD-19, **HIGH**) |
| All other sentinel-returning operations (`AddUserToGroup`, `ChangeUserPassword`, `GetUserEmail`, `GetUserFAX`, `GetUserLastLogon`, `GetUserPrinter`, `IsUserInGroup`) | `ADServiceResponse{Response=Failure, Message=ex.Message}` | Raw .NET exception text is flattened into free text | The distinction between a directory-connectivity failure and a business-rule rejection (for example a password-policy failure) is not preserved in `Response`, only in the free-text `Message`* |

## Logging on failure

| Surface | Location | Written content | Failure of the logger itself |
| --- | --- | --- | --- |
| None | — | No logging framework is initialized or invoked anywhere in `clsADUserServices.asmx.vb`, despite the `log4net 2.0.5` package reference in `packages.config` (referenced, never used — [tech-debt.md](tech-debt.md) TD-08). No `Trace.WriteLine`, `EventLog` write, or `StreamWriter` exists in source | Not applicable | Not applicable |

No mutating operation writes an audit trail of any kind — see [tech-debt.md](tech-debt.md) SEC-09. `m_LastMessage`, the one field that captures directory-failure detail, is written by the three shared helpers (`GetContext`, `GetUserPrincipal`, `GetGroupPrincipal`) but is never read or returned by any operation ([tech-debt.md](tech-debt.md) TD-07).

## Related documentation
- [Service index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Authentication and authorization](authentication-authorization.md)
- [Configuration](configuration.md)
- [Dependencies](dependencies.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with ** could not be confirmed; treat as unknown until verified.

## Sources
- `src\ADUserServices\clsADUserServices.asmx.vb`
- `src\ADUserServices\Web.config`
- `src\ADUserServices\Web.Release.config`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md`
